/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.talkback.labeling;

import android.annotation.TargetApi;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Build;
import android.support.v4.os.UserManagerCompat;
import android.text.TextUtils;
import android.util.Log;
import com.android.talkback.BuildConfig;
import com.android.utils.LogUtils;
import com.android.utils.labeling.LabelsTable;

import java.util.Locale;

/**
 * A content provider for accessing TalkBack custom label data.
 * <p>
 * The following operations are supported at each URI:
 * <ul>
 * <li>{@code AUTHORITY/labels}: query and insert.
 * <li>{@code AUTHORITY/labels/#}: query, update, and delete.
 * </ul>
 */
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class LabelProvider extends ContentProvider {
    public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".providers.LabelProvider";
    static final String LABELS_PATH = "labels";
    static final Uri LABELS_CONTENT_URI = new Uri.Builder()
            .scheme("content")
            .authority(AUTHORITY)
            .path(LABELS_PATH)
            .build();
    private static final Uri LABELS_ID_CONTENT_URI = Uri.withAppendedPath(LABELS_CONTENT_URI, "#");
    private static final String PACKAGE_SUMMARY_PATH = "packageSummary";
    private static final Uri PACKAGE_SUMMARY_URI = new Uri.Builder()
            .scheme("content")
            .authority(AUTHORITY)
            .path(PACKAGE_SUMMARY_PATH)
            .build();

    /* Codes for URI matching */
    static final int LABELS = 1;
    static final int LABELS_ID = 2;
    private static final int PACKAGE_SUMMARY = 3;

    private static final String UNKNOWN_URI_FORMAT_STRING = "Unknown URI: %s";
    private static final String NULL_URI_FORMAT_STRING = "URI is null";

    static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        sUriMatcher.addURI(AUTHORITY, LABELS_CONTENT_URI.getPath(), LABELS);
        sUriMatcher.addURI(AUTHORITY, LABELS_ID_CONTENT_URI.getPath(), LABELS_ID);
        sUriMatcher.addURI(AUTHORITY, PACKAGE_SUMMARY_URI.getPath(), PACKAGE_SUMMARY);
    }

    private SQLiteDatabase mDatabase;

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    /**
     * Inserts a label in the labels database.
     *
     * @param uri The content URI for labels.
     * @param values The values to insert for the new label.
     * @return The URI of the newly inserted label,
     *         or {@code null} if the insert failed.
     */
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        if (uri == null) {
            LogUtils.log(this, Log.WARN, NULL_URI_FORMAT_STRING);
            return null;
        }

        if (!UserManagerCompat.isUserUnlocked(getContext())) {
            return null;
        }

        switch (sUriMatcher.match(uri)) {
            case LABELS:
                initializeDatabaseIfNull();

                if (values == null) {
                    return null;
                }

                if (values.containsKey(LabelsTable.KEY_ID)) {
                    LogUtils.log(this, Log.WARN, "Label ID must be assigned by the database.");
                    return null;
                }

                long rowId = mDatabase.insert(LabelsTable.TABLE_NAME, null, values);

                if (rowId < 0) {
                    LogUtils.log(this, Log.WARN, "Failed to insert label.");
                    return null;
                } else {
                    return ContentUris.withAppendedId(LABELS_CONTENT_URI, rowId);
                }
            default:
                LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
                return null;
        }
    }

    /**
     * Queries for a label or multiple labels in the labels database.
     *
     * @param uri The URI representing the type of query to perform:
     *            {@code LABELS_CONTENT_URI} for a subset of all labels,
     *            {@code LABELS_ID_CONTENT_URI} for a specific label, or
     *            {@code PACKAGE_SUMMARY} for a label count per package.
     * @param projection The columns to return.
     * @param selection The WHERE clause for the query.
     * @param selectionArgs The arguments for the WHERE clause of the query.
     * @param sortOrder the ORDER BY clause for the query.
     * @return A cursor representing the data resulting from the query, or]
     *         {@code null} if the query failed to execute.
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        if (uri == null) {
            LogUtils.log(this, Log.WARN, NULL_URI_FORMAT_STRING);
            return null;
        }

        if (!UserManagerCompat.isUserUnlocked(getContext())) {
            return null;
        }

        final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        queryBuilder.setTables(LabelsTable.TABLE_NAME);

        String groupBy = null;

        switch (sUriMatcher.match(uri)) {
            case LABELS:
                if (TextUtils.isEmpty(sortOrder)) {
                    sortOrder = LabelsTable.KEY_ID;
                }
                break;
            case LABELS_ID:
                final String labelIdString = uri.getLastPathSegment();
                final int labelId;
                try {
                    labelId = Integer.parseInt(labelIdString);
                } catch (NumberFormatException e) {
                    LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
                    return null;
                }

                final String where = String.format(Locale.ROOT,
                        "%s = %d", LabelsTable.KEY_ID, labelId);
                queryBuilder.appendWhere(where);
                break;
            case PACKAGE_SUMMARY:
                projection = new String[] { LabelsTable.KEY_PACKAGE_NAME, "COUNT(*)" };
                groupBy = LabelsTable.KEY_PACKAGE_NAME;
                sortOrder = LabelsTable.KEY_PACKAGE_NAME;
                break;
            default:
                LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
                return null;
        }

        initializeDatabaseIfNull();

        return queryBuilder.query(mDatabase, projection, selection, selectionArgs, groupBy,
                null /* having */, sortOrder);
    }

    /**
     * Updates a label in the labels database.
     *
     * @param uri The URI matching {code LABELS_ID_CONTENT_URI} that represents
     *            the specific label to update.
     * @param values The values to use to update the label.
     * @param selection The WHERE clause for the query.
     * @param selectionArgs The arguments for the WHERE clause of the query.
     * @return The number of rows affected.
     */
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        if (uri == null) {
            LogUtils.log(this, Log.WARN, NULL_URI_FORMAT_STRING);
            return 0;
        }

        if (!UserManagerCompat.isUserUnlocked(getContext())) {
            return 0;
        }

        switch (sUriMatcher.match(uri)) {
            case LABELS: {
                initializeDatabaseIfNull();

                int result = mDatabase.update(LabelsTable.TABLE_NAME, values, selection,
                        selectionArgs);
                getContext().getContentResolver().notifyChange(uri, null /* observer */);
                return result;
            }
            case LABELS_ID: {
                initializeDatabaseIfNull();

                final String labelIdString = uri.getLastPathSegment();
                final int labelId;
                try {
                    labelId = Integer.parseInt(labelIdString);
                } catch (NumberFormatException e) {
                    LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
                    return 0;
                }

                final String where = String.format(Locale.ROOT,
                        "%s = %d", LabelsTable.KEY_ID, labelId);
                final int result = mDatabase.update(LabelsTable.TABLE_NAME, values,
                        combineSelectionAndWhere(selection, where), selectionArgs);

                getContext().getContentResolver().notifyChange(uri, null /* observer */);

                return result;
            }
            default:
                LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
                return 0;
        }
    }

    /**
     * Deletes a label in the labels database.
     *
     * @param uri The URI matching {code LABELS_ID_CONTENT_URI} that represents
     *            the specific label to delete.
     * @param selection The WHERE clause for the query.
     * @param selectionArgs The arguments for the WHERE clause of the query.
     * @return The number of rows affected.
     */
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        if (uri == null) {
            LogUtils.log(this, Log.WARN, NULL_URI_FORMAT_STRING);
            return 0;
        }

        if (!UserManagerCompat.isUserUnlocked(getContext())) {
            return 0;
        }

        switch (sUriMatcher.match(uri)) {
            case LABELS: {
                initializeDatabaseIfNull();

                int result = mDatabase.delete(LabelsTable.TABLE_NAME, selection, selectionArgs);
                getContext().getContentResolver().notifyChange(uri, null /* observer */);
                return result;
            }
            case LABELS_ID: {
                initializeDatabaseIfNull();

                final String labelIdString = uri.getLastPathSegment();
                final int labelId;
                try {
                    labelId = Integer.parseInt(labelIdString);
                } catch (NumberFormatException e) {
                    LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
                    return 0;
                }

                final String where = String.format(Locale.ROOT,
                        "%s = %d", LabelsTable.KEY_ID, labelId);
                final int result = mDatabase.delete(LabelsTable.TABLE_NAME,
                        combineSelectionAndWhere(selection, where), selectionArgs);

                getContext().getContentResolver().notifyChange(uri, null /* observer */);

                return result;
            }
            default:
                LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
                return 0;
        }
    }

    @Override
    public void shutdown() {
        if (mDatabase != null) {
            mDatabase.close();
        }
    }

    /**
     * Joins a selection clause with a where clause to form a larger selection
     * clause that represents the AND of the two clauses.
     *
     * @param selection The selection clause.
     * @param where The where clause.
     * @return The joined clause.
     */
    private String combineSelectionAndWhere(String selection, final String where) {
        if (TextUtils.isEmpty(where)) {
            return selection;
        } else if (TextUtils.isEmpty(selection)) {
            return where;
        }

        return String.format(Locale.ROOT, "(%s) AND (%s)", where, selection);
    }

    /**
     * Initializes the database (if not already initialized) when used.
     * <p>
     * Note: the database is automatically cleaned up by the kernel when the
     * process terminates.
     */
    private void initializeDatabaseIfNull() {
        if (mDatabase == null) {
            mDatabase = new LabelsDatabaseOpenHelper(getContext()).getWritableDatabase();
        }
    }

    /**
     * A helper for managing a SQLite database that stores label data.
     */
    private final class LabelsDatabaseOpenHelper extends SQLiteOpenHelper {
        private static final String DATABASE_NAME = "labelsDatabase.db";

        /*
         * If the database structure is modified and this value is changed, be
         * sure to implement the onUpgrade method for the database and each
         * relevant table that it includes.
         */
        private static final int DATABASE_VERSION = 3;

        public LabelsDatabaseOpenHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            LabelsTable.onCreate(db);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            LabelsTable.onUpgrade(db, oldVersion, newVersion);
        }
    }
}
